package memfile

import (
	"github.com/pkg/errors"
	"io"
	"io/fs"
	"os"
	"path"
	"strings"
	"sync"
	"sync/atomic"
)

var fileSystem = NewFileSystem()

type File struct {
	rwMutex    sync.RWMutex
	name       string
	isDir      bool
	fileMode   fs.FileMode
	data       []byte
	contentLen int
	items      map[string]*File
	at         int64
}

func newFile(name string, isDir bool) *File {
	if isDir {
		return &File{
			name:     name,
			isDir:    true,
			fileMode: os.ModePerm,
			items:    make(map[string]*File),
		}
	}
	return &File{
		name:     name,
		isDir:    false,
		fileMode: os.ModePerm,
		data:     []byte{},
	}
}

func NewFile(name string) *File {
	return newFile(name, false)
}

func NewDir(name string) *File {
	return newFile(name, true)
}

func NewFileSystem() *File {
	return NewDir("")
}

func (f *File) Name() string {
	return f.name
}

func (f *File) IsDir() bool {
	return f.isDir
}

func (f *File) Type() fs.FileMode {
	return f.fileMode
}

func (f *File) Info() (fs.FileInfo, error) {
	//TODO implement me
	panic("implement me")
}

func (f *File) Open() (*File, error) {
	atomic.StoreInt64(&f.at, 0)
	return f, nil
}

func (f *File) Close() error {
	if f == nil {
		return os.ErrInvalid
	}
	return nil
}

func (f *File) Write(p []byte) (n int, err error) {
	f.rwMutex.Lock()
	defer f.rwMutex.Unlock()
	if p == nil || len(p) == 0 {
		return
	}
	f.data = append(f.data, p...)
	f.contentLen = len(f.data)
	return len(p), err
}

func (f *File) Read(p []byte) (n int, err error) {
	if f.data == nil {
		return 0, nil
	}
	f.rwMutex.RLock()
	defer f.rwMutex.RUnlock()
	atomic.AddInt64(&f.at, int64(n))
	if f.data == nil || len(f.data) == 0 {
		return 0, nil
	}
	if f.at >= int64(f.contentLen) {
		return 0, io.EOF
	}
	n = copy(p, f.data[f.at:])
	atomic.AddInt64(&f.at, int64(n))
	return
}

func (f *File) Seek(offset int64, whence int) (int64, error) {
	f.rwMutex.Lock()
	defer f.rwMutex.Unlock()
	switch whence {
	case io.SeekStart:
		atomic.StoreInt64(&f.at, offset)
	case io.SeekCurrent:
		atomic.AddInt64(&f.at, offset)
	case io.SeekEnd:
		atomic.StoreInt64(&f.at, int64(f.contentLen)-offset)
	}
	return f.at, nil
}

func (f *File) Reset() {
	atomic.StoreInt64(&f.at, 0)
}

func (f *File) WriteString(s string) (n int, err error) {
	return f.Write([]byte(s))
}

func (f *File) GetChild(name string) (*File, error) {
	if !f.isDir {
		return nil, ErrNotDir
	}
	if f, ok := f.items[name]; ok {
		return f, nil
	}
	return nil, ErrNotFound
}

func (f *File) AddChild(name string, isDir bool) (*File, error) {
	if !f.isDir {
		return nil, ErrNotDir
	}
	nf := newFile(name, isDir)
	f.items[name] = nf
	return nf, nil
}

func (f *File) createFile(filePath string, isDir bool) (*File, error) {
	var err error
	dirPath, fileName := path.Split(filePath)
	sp := strings.Split(dirPath, "/")
	fd := f
	for _, p := range sp {
		if p == "" {
			break
		}
		fd, err = fd.GetChild(p)
		if err != nil {
			return nil, err
		}
	}
	return fd.AddChild(fileName, isDir)
}

func (f *File) GetContentLen() int {
	return f.contentLen
}

func (f *File) Create(filePath string) (*File, error) {
	return f.createFile(filePath, false)
}
func (f *File) CreateDir(filePath string) (*File, error) {
	return f.createFile(filePath, true)
}

func (f *File) CreateAllDir(filePath string) (*File, error) {
	sp := strings.Split(filePath, "/")
	fd := f
	for _, p := range sp {
		ff, err := fd.GetChild(p)
		if err != nil {
			fd, err = fd.AddChild(p, true)
			if err != nil {
				return nil, errors.Wrap(err, p)
			}
		} else {
			fd = ff
		}
	}
	return fd, nil
}

func (f *File) ReadDir(filePath string) ([]fs.DirEntry, error) {
	var err error
	sp := strings.Split(filePath, "/")
	fd := f
	for _, p := range sp {
		fd, err = fd.GetChild(p)
		if err != nil {
			return nil, err
		}
	}
	des := make([]fs.DirEntry, 0, len(fd.items))
	for _, v := range fd.items {
		des = append(des, v)
	}
	return des, nil
}

func Create(filePath string) (*File, error) {
	return fileSystem.Create(filePath)
}

func CreateDir(filePath string) (*File, error) {
	return fileSystem.CreateDir(filePath)
}

func CreateAllDir(filePath string) (*File, error) {
	return fileSystem.CreateAllDir(filePath)
}

func Open(filePath string) (*File, error) {
	var err error
	sp := strings.Split(filePath, "/")
	fd := fileSystem
	for _, p := range sp {
		fd, err = fd.GetChild(p)
		if err != nil {
			return nil, err
		}
	}
	return fd.Open()
}

func ReadDir(filePath string) ([]fs.DirEntry, error) {
	return fileSystem.ReadDir(filePath)
}
